Skip to content

Delphi

Many accounting systems, ERPs and points of sale in Ecuador are written in Delphi and run on the office’s own computers. They can issue electronic documents with Flashpi without an SDK: Flashpi is an HTTPS API that takes and returns JSON, and Delphi brings an HTTP client (THTTPClient) and a JSON library (System.JSON). This guide issues the invoice of the Quickstart from Delphi, with the shared demo issuer in the SRI test environment.

  • Delphi 10.4 or later, for Windows. The samples use THTTPClient (System.Net.HttpClient) and System.JSON; TNetHTTPClient, the component on the palette, has the same properties and methods. For Indy, see With Indy.
  • Windows 10 or 11, or Windows Server 2016 or later, on the computer that calls the API. On Windows, THTTPClient uses the TLS of Windows itself (see TLS 1.2 and Windows versions).
  • A test API key (fp_test_), created in the console’s API keys with Test chosen, with access to every issuer (scope account) and read and write access (see Authentication and API keys), and the demo issuer’s ID, from the console’s Overview (see Sandbox and demo issuer).
  • Best: one server calls Flashpi. The PCs ask a small service on your server (or on the ERP’s server) to issue, and only that server holds the key. The PCs never see it, and the server’s public address can be the only one in the key’s IP allowlist.
  • If each PC calls the API, keep the key on that PC only, readable by the Windows user that runs the system: an environment variable of that user (the samples read GetEnvironmentVariable('FLASHPI_API_KEY')) or, better, encrypted with the Windows Data Protection API (CryptProtectData), which only that user or that computer can decrypt.
  • If you sell your system to other companies, each customer creates its own Flashpi account and keys, and enters its key when you install the system. Never ship your own key inside the product.
  • Give each key the least it needs: an issuer-scoped key for the one RUC the system issues for, and an IP allowlist with the office’s public address when it is fixed.
  • Start with a test key (fp_test_): its documents have no tax effect. Use a live key (fp_live_) only on the computers that issue for real, once everything works.
  • Never write the key in a log or show it on screen. If it leaks, revoke it in the console: it stops working at once.

GET /v1/status needs no key, so it tells you whether the computer reaches the API:

var
Client: THTTPClient;
begin
Client := THTTPClient.Create;
try
// 200 {"status":"operational"}
ShowMessage(Client.Get('https://api.flashpi.com.ec/v1/status').ContentAsString);
finally
Client.Free;
end;
end;

If it fails with A security error occurred (WinHTTP error 12175), Windows could not set up TLS with the API: see TLS 1.2 and Windows versions.

Flashpi.pas
unit Flashpi;
interface
uses
System.SysUtils, System.Classes, System.JSON, System.Net.URLClient,
System.Net.HttpClient;
const
FlashpiApi = 'https://api.flashpi.com.ec';
function NewIdempotencyKey: string;
function Amount(const Value: Currency): string;
function Decimal(const Value: Double; Decimals: Integer): string;
function IssueDocument(const Path, Body, IdempotencyKey: string;
out Error: string): TJSONObject;
function GetDocument(const DocumentId: string; out Error: string): TJSONObject;
function SaveDocumentFile(const DocumentId, Kind, FileName: string;
out Error: string): Boolean;
implementation
// A new GUID without braces: the Idempotency-Key of one document
function NewIdempotencyKey: string;
begin
Result := Copy(TGUID.NewGuid.ToString, 2, 36);
end;
// An amount of money as the API takes it: a string with 2 decimals and a
// dot, whatever the regional settings of Windows say (a comma in Ecuador)
function Amount(const Value: Currency): string;
var
Format: TFormatSettings;
begin
Format := TFormatSettings.Create;
Format.DecimalSeparator := '.';
Result := CurrToStrF(Value, ffFixed, 2, Format);
end;
// A quantity or a unit price, with up to 6 decimals: a string with a dot
// (Currency keeps only 4 decimals, so this one takes a Double)
function Decimal(const Value: Double; Decimals: Integer): string;
var
Format: TFormatSettings;
begin
Format := TFormatSettings.Create;
Format.DecimalSeparator := '.';
Result := FloatToStrF(Value, ffFixed, 15, Decimals, Format);
end;
function NewClient: THTTPClient;
begin
Result := THTTPClient.Create;
Result.ConnectionTimeout := 10000; // milliseconds
// Longer than the sync mode's wait (15 seconds by default)
Result.ResponseTimeout := 30000;
end;
function Headers(const IdempotencyKey: string): TNetHeaders;
begin
Result := [TNetHeader.Create('Authorization',
'Bearer ' + GetEnvironmentVariable('FLASHPI_API_KEY')),
// Error messages in Spanish, to show them to the user
TNetHeader.Create('Accept-Language', 'es')];
if IdempotencyKey <> '' then
Result := Result + [TNetHeader.Create('Idempotency-Key', IdempotencyKey)];
end;
// The text of an error answer (RFC 9457 problem details) to show or log:
// detail and code, each field error, and the requestId for support
function ProblemText(const Response: IHTTPResponse; const Text: string): string;
var
Problem, Field: TJSONValue;
begin
Problem := TJSONObject.ParseJSONValue(Text);
if Problem = nil then
begin
// Not problem details, such as a proxy's error page: the status, and the
// request ID when the API sent one
Result := 'HTTP ' + IntToStr(Response.StatusCode);
if Response.HeaderValue['x-request-id'] <> '' then
Result := Result + sLineBreak + 'requestId: ' +
Response.HeaderValue['x-request-id'];
Exit;
end;
try
Result := Problem.GetValue<string>('detail') + ' (' +
Problem.GetValue<string>('code') + ')';
for Field in Problem.GetValue<TJSONArray>('errors') do
Result := Result + sLineBreak + Field.GetValue<string>('field') + ': ' +
Field.GetValue<string>('message');
Result := Result + sLineBreak + 'requestId: ' +
Problem.GetValue<string>('requestId');
finally
Problem.Free;
end;
end;
// One member of an error answer, such as its code, to branch on it ('' when
// it is missing)
function ProblemValue(const Text, Name: string): string;
var
Problem: TJSONValue;
begin
Result := '';
Problem := TJSONObject.ParseJSONValue(Text);
if Problem <> nil then
try
Result := Problem.GetValue<string>(Name, '');
finally
Problem.Free;
end;
end;
// Issues a document, retrying safely: the same body and the same
// Idempotency-Key on every attempt, so it is never issued twice. Returns the
// document (free it), or nil with Error set.
function IssueDocument(const Path, Body, IdempotencyKey: string;
out Error: string): TJSONObject;
var
Client: THTTPClient;
Source: TStringStream;
Response: IHTTPResponse;
Attempt, Wait, Status: Integer;
Text, DocumentId: string;
begin
Result := nil;
Error := '';
Client := NewClient;
try
Client.ContentType := 'application/json';
for Attempt := 1 to 5 do
begin
Wait := 1 shl (Attempt - 1); // 1, 2, 4 and 8 seconds between attempts
Response := nil;
// The JSON's UTF-8 bytes, without a byte order mark
Source := TStringStream.Create(Body, TEncoding.UTF8);
try
try
Response := Client.Post(FlashpiApi + Path, Source, nil,
Headers(IdempotencyKey));
except
// No answer: Flashpi may have received the request, so send it again
on E: ENetException do
Error := E.Message;
end;
finally
Source.Free;
end;
if Response <> nil then
begin
Status := Response.StatusCode;
Text := Response.ContentAsString(TEncoding.UTF8);
if (Status = 200) or (Status = 202) then
Exit(TJSONObject.ParseJSONValue(Text) as TJSONObject);
Error := ProblemText(Response, Text);
if Status < 500 then
begin
DocumentId := '';
if ProblemValue(Text, 'code') = 'idempotency_key_reused' then
DocumentId := ProblemValue(Text, 'documentId');
if DocumentId <> '' then
// The key already created a document: read it instead of issuing again
Exit(GetDocument(DocumentId, Error));
if (Status = 429) and
(StrToIntDef(Response.HeaderValue['Retry-After'], 0) <= 60) then
Wait := StrToIntDef(Response.HeaderValue['Retry-After'], 1)
else if ProblemValue(Text, 'code') <> 'idempotency_key_in_progress' then
Exit; // Fix the request first: sent unchanged, it gets the same answer
end;
end;
if Attempt < 5 then
Sleep(Wait * 1000);
end;
finally
Client.Free;
end;
end;
// Reads a document again: its status, its access key, its totals...
function GetDocument(const DocumentId: string; out Error: string): TJSONObject;
var
Client: THTTPClient;
Response: IHTTPResponse;
Text: string;
begin
Result := nil;
Error := '';
Client := NewClient;
try
try
Response := Client.Get(FlashpiApi + '/v1/documents/' + DocumentId, nil,
Headers(''));
except
on E: ENetException do
begin
Error := E.Message;
Exit;
end;
end;
Text := Response.ContentAsString(TEncoding.UTF8);
if Response.StatusCode = 200 then
Result := TJSONObject.ParseJSONValue(Text) as TJSONObject
else
Error := ProblemText(Response, Text);
finally
Client.Free;
end;
end;
// Saves the RIDE ('pdf') or the XML ('xml') of a document in a file
function SaveDocumentFile(const DocumentId, Kind, FileName: string;
out Error: string): Boolean;
var
Client: THTTPClient;
Response: IHTTPResponse;
Output: TFileStream;
begin
Result := False;
Error := '';
Client := NewClient;
try
try
Response := Client.Get(FlashpiApi + '/v1/documents/' + DocumentId + '/' +
Kind, nil, Headers(''));
except
on E: ENetException do
begin
Error := E.Message;
Exit;
end;
end;
if Response.StatusCode <> 200 then
begin
Error := ProblemText(Response, Response.ContentAsString(TEncoding.UTF8));
Exit;
end;
// The file's bytes as they arrived
Output := TFileStream.Create(FileName, fmCreate);
try
Output.CopyFrom(Response.ContentStream, 0);
finally
Output.Free;
end;
Result := True;
finally
Client.Free;
end;
end;
end.
  1. Give the invoice its Idempotency-Key and save it with the invoice before the first attempt. A retry, even after the program or the PC restarted, sends the same key, and Flashpi answers it with the document the first attempt created instead of issuing a second one (see Idempotency). Here the key lives in a field idem_key of the invoices table. With cached updates (TClientDataSet, FireDAC’s CachedUpdates), call ApplyUpdates after each Post, so the key reaches the database before IssueDocument() is called.

  2. Build the JSON body with System.JSON, which escapes the texts for you. Amounts and quantities are decimal strings with a dot: Amount() for money, with 2 decimals, and Decimal() for quantities and unit prices, which take up to 6. You send the price and the VAT rate; Flashpi computes the subtotal, the VAT (67.50) and the total (517.50), and the payments must add up to that total. For several lines, add one item per line to the items array.

  3. Send it with IssueDocument(), which retries on its own when that is safe.

// 2. The body, with the values your system keeps in its tables
function InvoiceBody: string;
var
Invoice, Customer, Item, Tax, Payment, Options: TJSONObject;
Items, Taxes, Payments: TJSONArray;
begin
Invoice := TJSONObject.Create;
try
Invoice.AddPair('issuerId', '<demo issuer id>'); // from the console's Overview
Invoice.AddPair('establishment', '001');
Invoice.AddPair('emissionPoint', '001');
Customer := TJSONObject.Create;
Customer.AddPair('idType', 'ruc');
Customer.AddPair('id', '1790000000001');
Customer.AddPair('name', 'Supermercados La Pradera S.A.');
Invoice.AddPair('customer', Customer);
Tax := TJSONObject.Create;
Tax.AddPair('type', 'vat');
Tax.AddPair('rate', '15');
Taxes := TJSONArray.Create;
Taxes.AddElement(Tax);
Item := TJSONObject.Create;
Item.AddPair('code', 'SRV-001');
Item.AddPair('description', 'Monthly accounting services');
Item.AddPair('quantity', Decimal(1, 2));
Item.AddPair('unitPrice', Decimal(450, 2));
Item.AddPair('taxes', Taxes);
Items := TJSONArray.Create;
Items.AddElement(Item);
Invoice.AddPair('items', Items);
Payment := TJSONObject.Create;
Payment.AddPair('method', 'other_financial_system');
Payment.AddPair('amount', Amount(517.50));
Payments := TJSONArray.Create;
Payments.AddElement(Payment);
Invoice.AddPair('payments', Payments);
Options := TJSONObject.Create;
Options.AddPair('waitForAuthorization', TJSONBool.Create(True));
Invoice.AddPair('options', Options);
Result := Invoice.ToJSON;
finally
Invoice.Free; // also frees every object and array added to it
end;
end;
procedure IssueInvoice(Invoices: TDataSet);
var
Key, Error: string;
Document: TJSONObject;
begin
// 1. One key per invoice, saved before the first attempt
Key := Invoices.FieldByName('idem_key').AsString;
if Key = '' then
begin
Key := NewIdempotencyKey;
Invoices.Edit;
Invoices.FieldByName('idem_key').AsString := Key;
Invoices.Post;
end;
// 3. Send it
Document := IssueDocument('/v1/invoices', InvoiceBody, Key, Error);
if Document = nil then
raise Exception.Create(Error);
try
// Keep the document's id: it reads the document, its RIDE and its XML
Invoices.Edit;
Invoices.FieldByName('flashpi_id').AsString := Document.GetValue<string>('id');
Invoices.FieldByName('doc_status').AsString := Document.GetValue<string>('status');
Invoices.FieldByName('doc_number').AsString := Document.GetValue<string>('number');
Invoices.FieldByName('access_key').AsString := Document.GetValue<string>('accessKey');
Invoices.Post;
finally
Document.Free;
end;
end;

IssueInvoice() needs Data.DB, System.SysUtils, System.JSON and the Flashpi unit in its uses. The same IssueDocument() issues the other documents: only the path and the body change (/v1/credit-notes, /v1/withholdings and the rest, see Issuing documents).

Answer What it means What to do
200 OK Sync mode only: the SRI settled the document within the wait. status is authorized, rejected or failed. authorized: download and print the RIDE. rejected: the SRI’s reasons are in sriMessages. failed: failure says why.
202 Accepted The document is stored and on its way: processing, or pending_sri when the wait ended before the SRI answered. Keep its id and follow it (see Sync mode, polling or webhooks). Nothing is lost.
4xx An error that does not go away on its own, as a problem details object. Show detail, branch on code, fix and send again. The only ones worth retrying unchanged are 409 idempotency_key_in_progress and a 429 with a short Retry-After.
5xx, or no answer The request may or may not have arrived. Send it again with the same key and the same body, waiting longer each time.

THTTPClient raises no exception for an error status: the answer arrives with its status, and only a missing answer (a network error, a timeout or a TLS failure) raises an ENetException. The document the API returns is the one in the Quickstart: id, status, number (001-001-000004821), accessKey (the 49-digit access key), totals, authorization, sriMessages and the rest. An error answer looks like this, in Spanish because the unit sends Accept-Language: es, and ProblemText() turns it into one text:

{
"type": "https://flashpi.com.ec/docs/errors/validation_failed",
"title": "Error de validación",
"status": 422,
"detail": "Uno o más campos no son válidos. Revisa errors para ver el detalle.",
"instance": "/v1/invoices",
"code": "validation_failed",
"errors": [
{ "field": "customer.id", "code": "invalid_check_digit", "message": "El dígito verificador de este número de identificación es incorrecto." }
],
"requestId": "01a1412b-9c03-7e55-b1a8-27f4d6e0c913"
}

Branch on code and errors[].code, which never change with the language; title, detail and message are texts for people. Every code has its page in Errors.

IssueDocument() follows the rules of Idempotency:

  • It retries only what is safe to retry: no answer (a network error or a timeout), a 5xx, 409 idempotency_key_in_progress, and a 429 whose Retry-After is at most 60 seconds. A 429 that asks for more, such as the demo issuer’s daily quota, stops and returns the error.
  • Always with the same key and the same body. If the first attempt did arrive, Flashpi answers the retry with the same document and the header Idempotent-Replayed: true, never a second invoice. With the same key and a different body, the answer is 422 idempotency_key_reused.
  • After the last attempt the invoice keeps its key, so sending it later is still safe. Within the 24 hours that Flashpi remembers the key, if the first attempt issued the document, the same request gets that answer again. After that, the answer is 422 idempotency_key_reused with the documentId of the document the key created, and IssueDocument() reads that document. If you changed the invoice within the 24 hours, the 422 has no documentId: do not give the invoice a new key, because the first attempt may have issued it; look for it in the console first.
  • ResponseTimeout (30 seconds here) must be longer than the sync mode’s wait: 15 seconds by default, up to waitTimeoutSeconds (30 at most), plus a few seconds. Otherwise the program gives up just before the answer arrives.
  • The sync mode (options.waitForAuthorization, as in the sample) suits a point of sale that prints the RIDE at once: the request waits up to 15 seconds for the SRI (waitTimeoutSeconds, from 1 to 30). When the SRI takes longer, the answer is a 202 with pending_sri, and the document goes on by itself (see Sync and async modes).
  • Webhooks push each authorization and rejection to an HTTPS address of yours, so they need a server that the internet can reach: a desktop PC cannot receive them. If your system has a server or a web service, receive them there (see Webhooks).
  • Polling works from any PC: read the document with GetDocument() until its status is final (authorized, rejected or failed), for example from a TTimer. Wait a few seconds between reads at first, then longer, for example a minute, because when the SRI is down a document can take hours or days: Flashpi tries to submit it for up to 72 hours from its creation and waits up to 72 hours from the submission for the SRI’s decision (see Limits). After the system was closed, GET /v1/events lists the events you missed. A failed document raises no event yet: also look for it with GET /v1/documents?status=failed.

Once the document is authorized, download its RIDE (the PDF) with GET /v1/documents/{id}/pdf and its authorized XML with GET /v1/documents/{id}/xml. Flashpi does not email them to the buyer yet, so the issuer delivers them (see RIDE and email).

var
Id, FileName, Error: string;
begin
Id := Invoices.FieldByName('flashpi_id').AsString;
FileName := 'C:\Invoices\factura-' + Invoices.FieldByName('doc_number').AsString;
if not SaveDocumentFile(Id, 'pdf', FileName + '.pdf', Error) then
ShowMessage(Error);
SaveDocumentFile(Id, 'xml', FileName + '.xml', Error);
end;

The RIDE is rendered right after the authorization, usually a few seconds later. Until then the answer is 409 ride_not_available: try again a moment later.

The API accepts TLS 1.2 and TLS 1.3 only, with current cipher suites (ECDHE with AES-GCM or ChaCha20). On Windows, THTTPClient and TNetHTTPClient use WinHTTP, the TLS of Windows itself, so what counts is the Windows version, not Delphi’s:

  • Windows 10 and 11, and Windows Server 2016 or later connect as they are.
  • Windows 7 and Windows Server 2008 R2 cannot connect, even after turning TLS 1.2 on in WinHTTP (update KB3140245): they lack those cipher suites, so the handshake fails. Older versions cannot either.
  • Other versions out of Microsoft’s support may fail the same way. Check the connection on each computer before you rely on it.

On a computer that cannot connect, move the call to a service on a supported Windows (the server option of Keep the API key safe), update that computer’s Windows, or use Indy, which brings its own TLS (see below).

  • The API reads and writes UTF-8. Delphi’s strings are Unicode: TStringStream.Create(Body, TEncoding.UTF8) sends the UTF-8 bytes, and ContentAsString(TEncoding.UTF8) reads the answer.
  • Declare application/json or application/json; charset=utf-8. A charset that does not start with utf-, such as windows-1252 or latin1, is refused with 415 unsupported_media_type.
  • Text fields take up to 300 characters, in one line and without emoji (see Limits). Trim the values of fixed-length fields.

Many older systems call HTTP with Indy (TIdHTTP). It works with Flashpi too:

  • TLS: give TIdHTTP a TIdSSLIOHandlerSocketOpenSSL with SSLOptions.SSLVersions := [sslvTLSv1_2], and have it verify the server’s certificate and its exact name, as in the sample: without that, Indy accepts any certificate and the API key can be intercepted. cacert.pem is Mozilla’s bundle of certificate authorities (published by curl.se), which includes the Amazon roots that sign the API’s certificate, and VerifyPeer is a method, for example of the form, because OnVerifyPeer is an event. The sample needs an Indy 10.6 that sends SNI, such as the current one on GitHub: the API requires SNI, and an older Indy cannot connect. Indy uses OpenSSL instead of the TLS of Windows, so it also connects from versions of Windows that WinHTTP cannot, as long as its OpenSSL DLLs (libeay32.dll and ssleay32.dll) sit next to the EXE. That OpenSSL 1.0.2 is out of support: prefer THTTPClient where Windows allows it.
  • Error answers: by default TIdHTTP raises EIdHTTPProtocolException on a 4xx or 5xx. Add hoNoProtocolErrorException and hoWantProtocolErrorContent to HTTPOptions to read the problem details, and the status from ResponseCode.
  • Headers and body: Request.ContentType := 'application/json', Request.CharSet := 'utf-8', Authorization and Idempotency-Key with Request.CustomHeaders.AddValue(), and the body as a TStringStream in UTF-8, as above.
Http.IOHandler := SSL; // a TIdSSLIOHandlerSocketOpenSSL
SSL.SSLOptions.Mode := sslmClient;
SSL.SSLOptions.SSLVersions := [sslvTLSv1_2];
// Check the server's certificate: Indy accepts any certificate otherwise
SSL.SSLOptions.RootCertFile := ExtractFilePath(ParamStr(0)) + 'cacert.pem';
SSL.SSLOptions.VerifyMode := [sslvrfPeer];
SSL.SSLOptions.VerifyDepth := 9;
SSL.OnVerifyPeer := VerifyPeer;
Http.HTTPOptions := Http.HTTPOptions + [hoNoProtocolErrorException,
hoWantProtocolErrorContent];
Http.ConnectTimeout := 10000;
Http.ReadTimeout := 30000;
Http.Request.ContentType := 'application/json';
Http.Request.CharSet := 'utf-8';
Http.Request.CustomHeaders.AddValue('Authorization',
'Bearer ' + GetEnvironmentVariable('FLASHPI_API_KEY'));
Http.Request.CustomHeaders.AddValue('Idempotency-Key', Key);
Answer := Http.Post('https://api.flashpi.com.ec/v1/invoices', Source);
Status := Http.ResponseCode;
// A chain that cacert.pem trusts (AOk), ending in a certificate issued to
// exactly api.flashpi.com.ec. EndsText is in System.StrUtils.
function TForm1.VerifyPeer(Certificate: TIdX509; AOk: Boolean;
ADepth, AError: Integer): Boolean;
begin
Result := AOk and ((ADepth > 0) or
EndsText('/CN=api.flashpi.com.ec', Certificate.Subject.OneLine));
end;