Ir al contenido

Delphi

Muchos sistemas contables, ERP y puntos de venta de Ecuador están escritos en Delphi y corren en las propias computadoras de la oficina. Pueden emitir comprobantes electrónicos con Flashpi sin un SDK: Flashpi es una API HTTPS que recibe y devuelve JSON, y Delphi trae un cliente HTTP (THTTPClient) y una librería de JSON (System.JSON). Esta guía emite la factura del Inicio rápido desde Delphi, con el emisor demo compartido en el ambiente de pruebas del SRI.

  • Delphi 10.4 o posterior, para Windows. Los ejemplos usan THTTPClient (System.Net.HttpClient) y System.JSON; TNetHTTPClient, el componente de la paleta, tiene las mismas propiedades y métodos. Para Indy, consulta Con Indy.
  • Windows 10 u 11, o Windows Server 2016 o posterior, en la computadora que llama a la API. En Windows, THTTPClient usa el TLS del propio Windows (consulta TLS 1.2 y versiones de Windows).
  • Una API key de prueba (fp_test_), creada en API keys de la consola con Pruebas elegido, con alcance Toda la cuenta (account) y acceso de lectura y escritura (consulta Autenticación y API keys), y el ID del emisor demo, del Resumen de la consola (consulta Sandbox y emisor demo).
  • Lo mejor: un servidor llama a Flashpi. Las PC le piden a un pequeño servicio en tu servidor (o en el servidor del ERP) que emita, y solo ese servidor tiene la llave. Las PC nunca la ven, y la dirección pública del servidor puede ser la única de la lista de IP permitidas de la llave.
  • Si cada PC llama a la API, guarda la llave solo en esa PC, legible por el usuario de Windows que usa el sistema: una variable de entorno de ese usuario (los ejemplos leen GetEnvironmentVariable('FLASHPI_API_KEY')) o, mejor, cifrada con la API de protección de datos de Windows (CryptProtectData), que solo ese usuario o esa computadora pueden descifrar.
  • Si vendes tu sistema a otras empresas, cada cliente crea su propia cuenta y sus llaves de Flashpi, e ingresa su llave al instalar el sistema. Nunca incluyas tu propia llave en el producto.
  • Dale a cada llave solo lo que necesita: una llave con alcance de un emisor para el único RUC por el que emite el sistema, y una lista de IP permitidas con la dirección pública de la oficina cuando es fija.
  • Empieza con una llave de prueba (fp_test_): sus comprobantes no tienen efecto tributario. Usa una llave de producción (fp_live_) solo en las computadoras que emiten de verdad, cuando todo funcione.
  • Nunca escribas la llave en un log ni la muestres en pantalla. Si se filtra, revócala en la consola: deja de funcionar al instante.

GET /v1/status no necesita llave, así que te dice si la computadora llega a la 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;

Si falla con A security error occurred (error 12175 de WinHTTP), Windows no pudo establecer TLS con la API: consulta TLS 1.2 y versiones de Windows.

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. Dale a la factura su Idempotency-Key y guárdala con la factura antes del primer intento. Un reintento, incluso después de reiniciar el programa o la PC, envía la misma clave, y Flashpi le responde con el comprobante que creó el primer intento en lugar de emitir otro (consulta Idempotencia). Aquí la clave vive en un campo idem_key de la tabla de facturas. Con actualizaciones en caché (TClientDataSet, CachedUpdates de FireDAC), llama a ApplyUpdates después de cada Post, para que la clave llegue a la base de datos antes de llamar a IssueDocument().

  2. Arma el cuerpo JSON con System.JSON, que escapa los textos por ti. Los montos y las cantidades son strings decimales con punto: Amount() para el dinero, con 2 decimales, y Decimal() para las cantidades y los precios unitarios, que admiten hasta 6. Envías el precio y la tarifa de IVA; Flashpi calcula el subtotal, el IVA (67.50) y el total (517.50), y los pagos deben sumar ese total. Para varias líneas, agrega un ítem por línea al arreglo items.

  3. Envíala con IssueDocument(), que reintenta sola cuando es seguro hacerlo.

// 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() necesita Data.DB, System.SysUtils, System.JSON y la unidad Flashpi en su uses. La misma IssueDocument() emite los demás comprobantes: solo cambian la ruta y el cuerpo (/v1/credit-notes, /v1/withholdings y los demás, consulta Emitir comprobantes).

Respuesta Qué significa Qué hacer
200 OK Solo en modo síncrono: el SRI resolvió el comprobante dentro de la espera. status es authorized, rejected o failed. authorized: descarga e imprime el RIDE. rejected: los motivos del SRI están en sriMessages. failed: failure dice por qué.
202 Accepted El comprobante quedó guardado y sigue su curso: processing, o pending_sri cuando la espera terminó antes de que el SRI respondiera. Guarda su id y síguelo (consulta Modo síncrono, consultas o webhooks). No se pierde nada.
4xx Un error que no se resuelve solo, como un objeto problem details. Muestra detail, decide según code, corrige y vuelve a enviar. Solo vale la pena reintentar sin cambios 409 idempotency_key_in_progress y un 429 con un Retry-After corto.
5xx, o sin respuesta La solicitud pudo llegar o no. Envíala otra vez con la misma clave y el mismo cuerpo, esperando más cada vez.

THTTPClient no lanza una excepción por un estado de error: la respuesta llega con su estado, y solo la falta de respuesta (un error de red, un tiempo de espera agotado o una falla de TLS) lanza una ENetException. El comprobante que devuelve la API es el del Inicio rápido: id, status, number (001-001-000004821), accessKey (la clave de acceso de 49 dígitos), totals, authorization, sriMessages y lo demás. Una respuesta de error se ve así, en español porque la unidad envía Accept-Language: es, y ProblemText() la convierte en un solo texto:

{
"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"
}

Decide según code y errors[].code, que nunca cambian con el idioma; title, detail y message son textos para personas. Cada código tiene su página en Errores.

IssueDocument() sigue las reglas de Idempotencia:

  • Solo reintenta lo que es seguro reintentar: sin respuesta (un error de red o un tiempo de espera agotado), un 5xx, 409 idempotency_key_in_progress y un 429 cuyo Retry-After es de 60 segundos o menos. Un 429 que pide más, como la cuota diaria del emisor demo, se detiene y devuelve el error.
  • Siempre con la misma clave y el mismo cuerpo. Si el primer intento sí llegó, Flashpi responde al reintento con el mismo comprobante y el header Idempotent-Replayed: true, nunca con una segunda factura. Con la misma clave y otro cuerpo, la respuesta es 422 idempotency_key_reused.
  • Después del último intento la factura conserva su clave, así que enviarla más tarde sigue siendo seguro. Dentro de las 24 horas en que Flashpi recuerda la clave, si el primer intento emitió el comprobante, la misma solicitud recibe otra vez esa respuesta. Después, la respuesta es 422 idempotency_key_reused con el documentId del comprobante que creó la clave, e IssueDocument() lee ese comprobante. Si cambiaste la factura dentro de las 24 horas, el 422 no trae documentId: no le des una clave nueva, porque el primer intento pudo haberla emitido; búscala primero en la consola.
  • ResponseTimeout (30 segundos aquí) debe ser mayor que la espera del modo síncrono: 15 segundos por defecto, hasta waitTimeoutSeconds (30 como máximo), más unos segundos. Si no, el programa se rinde justo antes de que llegue la respuesta.
  • El modo síncrono (options.waitForAuthorization, como en el ejemplo) sirve para un punto de venta que imprime el RIDE enseguida: la solicitud espera hasta 15 segundos al SRI (waitTimeoutSeconds, de 1 a 30). Cuando el SRI tarda más, la respuesta es un 202 con pending_sri, y el comprobante sigue por su cuenta (consulta Modo síncrono y asíncrono).
  • Los webhooks envían cada autorización y cada rechazo a una dirección HTTPS tuya, así que necesitan un servidor al que se pueda llegar desde internet: una PC de escritorio no puede recibirlos. Si tu sistema tiene un servidor o un servicio web, recíbelos ahí (consulta Webhooks).
  • Las consultas funcionan desde cualquier PC: lee el comprobante con GetDocument() hasta que su estado sea final (authorized, rejected o failed), por ejemplo desde un TTimer. Espera unos segundos entre lecturas al principio y luego más, por ejemplo un minuto, porque cuando el SRI está caído un comprobante puede tardar horas o días: Flashpi intenta entregarlo al SRI hasta 72 horas desde su creación y espera su decisión hasta 72 horas desde el envío (consulta Límites). Después de que el sistema estuvo cerrado, GET /v1/events lista los eventos que te perdiste. Un comprobante failed todavía no genera evento: búscalo también con GET /v1/documents?status=failed.

Cuando el comprobante está authorized, descarga su RIDE (el PDF) con GET /v1/documents/{id}/pdf y su XML autorizado con GET /v1/documents/{id}/xml. Flashpi todavía no se los envía por correo al comprador, así que los entrega el emisor (consulta RIDE y correo).

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;

El RIDE se genera justo después de la autorización, normalmente unos segundos más tarde. Hasta entonces la respuesta es 409 ride_not_available: vuelve a intentarlo un momento después.

La API solo acepta TLS 1.2 y TLS 1.3, con conjuntos de cifrado actuales (ECDHE con AES-GCM o ChaCha20). En Windows, THTTPClient y TNetHTTPClient usan WinHTTP, el TLS del propio Windows, así que lo que cuenta es la versión de Windows, no la de Delphi:

  • Windows 10 y 11, y Windows Server 2016 o posterior se conectan tal como están.
  • Windows 7 y Windows Server 2008 R2 no pueden conectarse, ni siquiera después de activar TLS 1.2 en WinHTTP (actualización KB3140245): les faltan esos conjuntos de cifrado, así que el handshake falla. Las versiones anteriores tampoco.
  • Otras versiones sin soporte de Microsoft pueden fallar igual. Comprueba la conexión en cada computadora antes de confiar en ella.

En una computadora que no puede conectarse, mueve la llamada a un servicio en un Windows con soporte (la opción del servidor de Protege la API key), actualiza el Windows de esa computadora o usa Indy, que trae su propio TLS (consulta abajo).

  • La API lee y escribe UTF-8. Las strings de Delphi son Unicode: TStringStream.Create(Body, TEncoding.UTF8) envía los bytes en UTF-8, y ContentAsString(TEncoding.UTF8) lee la respuesta.
  • Declara application/json o application/json; charset=utf-8. Un charset que no empiece por utf-, como windows-1252 o latin1, se rechaza con 415 unsupported_media_type.
  • Los campos de texto admiten hasta 300 caracteres, en una línea y sin emojis (consulta Límites). Recorta los valores de los campos de largo fijo.

Muchos sistemas más antiguos llaman por HTTP con Indy (TIdHTTP). También funciona con Flashpi:

  • TLS: dale a TIdHTTP un TIdSSLIOHandlerSocketOpenSSL con SSLOptions.SSLVersions := [sslvTLSv1_2], y haz que verifique el certificado del servidor y su nombre exacto, como en el ejemplo: sin eso, Indy acepta cualquier certificado y la API key puede ser interceptada. cacert.pem es el paquete de autoridades de certificación de Mozilla (lo publica curl.se), que incluye las raíces de Amazon que firman el certificado de la API, y VerifyPeer es un método, por ejemplo del formulario, porque OnVerifyPeer es un evento. El ejemplo necesita un Indy 10.6 que envíe SNI, como el actual de GitHub: la API lo exige, y un Indy más antiguo no se conecta. Indy usa OpenSSL en lugar del TLS de Windows, así que también se conecta desde versiones de Windows que WinHTTP no puede, siempre que sus DLL de OpenSSL (libeay32.dll y ssleay32.dll) estén junto al EXE. Ese OpenSSL 1.0.2 ya no tiene soporte: prefiere THTTPClient donde Windows lo permita.
  • Respuestas de error: por defecto TIdHTTP lanza EIdHTTPProtocolException ante un 4xx o 5xx. Agrega hoNoProtocolErrorException y hoWantProtocolErrorContent a HTTPOptions para leer el problem details, y el estado en ResponseCode.
  • Headers y cuerpo: Request.ContentType := 'application/json', Request.CharSet := 'utf-8', Authorization e Idempotency-Key con Request.CustomHeaders.AddValue(), y el cuerpo como un TStringStream en UTF-8, como arriba.
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;