Examples
Six concrete objectives, each worked end-to-end as a sequence of DPF API calls.
Overview
Each objective below is self-contained but builds on the ones before it — together they walk a single workspace from account creation through a scheduled ingestion pipeline and into aggregated reporting. Run them in order the first time through:
Conventions Used Below
- Base URL is
https://api.dpf-it.com. Every request/response is JSON. - Success responses are shaped
{ "success": true, "data": {...} }; errors are{ "error": { "code": "...", "message": "..." } }. YOUR_DPF_JWT_TOKENis thetokenfrom Objective 1's login call. Pass it asAuthorization: Bearer YOUR_DPF_JWT_TOKENon every authenticated request below.YOUR_WORKSPACE_IDis theworkspaceIdcreated in Objective 1 — reused by every other objective on this page.- Most endpoints (
/data-specs,/connections,/job-triggers,/workspaces) are multi-action: one URL, with anactionfield in the JSON body selecting the operation. - Full request/response schemas are in the API Docs; this page is the "how do these calls chain together" companion to that reference.
1Register a User & Verify OTP
Create an account, verify it with the one-time code emailed on registration, log in for a JWT, and create the workspace that every other objective on this page will load data into and query.
-
Register
termsAcceptedmust betrueor registration is rejected. This sends a 6-digit OTP to the given email.curl -X POST https://api.dpf-it.com/auth/register \ -H "Content-Type: application/json" \ -d '{ "email": "jane@example.com", "password": "SecurePassword123", "firstName": "Jane", "lastName": "Doe", "termsAccepted": true }' # 201 Created # { "success": true, "data": { "userId": "...", "email": "jane@example.com", ... } } -
Verify the OTP
Use the 6-digit code from the verification email. If it expired or never arrived, call
POST /auth/resend-otpwith just{"email": "..."}.curl -X POST https://api.dpf-it.com/auth/verify-otp \ -H "Content-Type: application/json" \ -d '{ "email": "jane@example.com", "otp": "123456" }' # 200 OK # { "success": true, "data": { "message": "Email verified successfully" } } -
Log in
Returns a 24h-lived access
tokenplus a ~90-dayrefreshToken(exchange it atPOST /auth/refreshonce the access token expires). Save thetoken— every call below sends it asAuthorization: Bearer ....curl -X POST https://api.dpf-it.com/auth/login \ -H "Content-Type: application/json" \ -d '{ "email": "jane@example.com", "password": "SecurePassword123" }' # 200 OK # { # "success": true, # "data": { # "userId": "550e8400-e29b-41d4-a716-446655440000", # "token": "eyJhbGciOiJIUzI1NiIs...", # "refreshToken": "q8c-BbPEpIF7ai-FrYUZ_a4acEk7finhMGHQ5vUvBvg", # ... # } # } -
Create a workspace
Creating a workspace also provisions its Iceberg catalog namespace — you'll need that
workspaceIdfor every remaining objective on this page.curl -X POST https://api.dpf-it.com/workspaces \ -H "Authorization: Bearer YOUR_DPF_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "action": "create", "name": "Sales Analytics", "description": "Workspace used throughout the DPF examples" }' # 201 Created # { "success": true, "data": { "workspaceId": "550e8400-e29b-41d4-a716-446655440099", ... } }
workspaceId — for
550e8400-e29b-41d4-a716-446655440099 that's 446655440099. No lookup
call needed; it's a pure string operation on the ID you already have.
2Load a File into a New Table, Schema Inferred
This is Flow A from the /data-specs endpoint with targetOption: "auto-infer":
give DPF a sample file, and AI infers the target schema, generates parsing/transform code, and
loads the sample — creating a brand-new Iceberg table in the process. No pre-existing table or
schema file needed.
-
Create the spec
Returns a presigned upload URL for the sample file (valid 1 hour, 100 KB max — a
representative sample is enough, DPF re-runs this same logic against full files later).
curl -X POST https://api.dpf-it.com/data-specs \ -H "Authorization: Bearer YOUR_DPF_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "action": "create-spec", "workspaceId": "YOUR_WORKSPACE_ID", "specName": "Customer Signups", "sampleFileName": "customers.csv", "targetOption": "auto-infer", "computeSize": "small", "description": "Load new customer signups into a fresh Iceberg table" }' # 201 Created # { # "success": true, # "data": { # "specId": "550e8400-e29b-41d4-a716-446655440003", # "signedUrls": { "customers.csv": "https://dpf-specs.s3.us-east-1.amazonaws.com/...?X-Amz-Signature=..." }, # "expiresIn": 3600, # ... # } # } -
Upload the sample file
customers.csv:
PUT it straight to the signed URL from step 1 (it's a standard S3 presigned PUT, not a DPF endpoint — noid,name,email,signup_date 1,Acme Corp,ops@acme.test,2026-07-01 2,Beta Inc,hello@beta.test,2026-07-01 3,Globex LLC,contact@globex.test,2026-07-02Authorizationheader):curl -X PUT "SIGNED_URL_FROM_STEP_1" \ --upload-file customers.csv -
Start analysis
Kicks off schema inference + code generation, then (by default) loads the sample and starts
the Glue data-load job. Returns immediately — poll
get-statusfor progress.curl -X POST https://api.dpf-it.com/data-specs \ -H "Authorization: Bearer YOUR_DPF_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "action": "start-analysis", "workspaceId": "YOUR_WORKSPACE_ID", "specName": "Customer Signups" }' # 202 Accepted # { "success": true, "data": { "specId": "...", "status": "processing", ... } } -
Poll the spec's status
Poll with
specIduntilstatusisready(orfailed). Onceready,lastJobIdpoints at the data-load run itself.curl -X POST https://api.dpf-it.com/data-specs \ -H "Authorization: Bearer YOUR_DPF_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "action": "get-status", "specId": "550e8400-e29b-41d4-a716-446655440003" }' # 200 OK # { "success": true, "data": { "status": "ready", "lastJobId": "550e8400-e29b-41d4-a716-446655440005", ... } } -
Poll the load itself
Poll the same endpoint with
jobIdfor the Glue run's own progress and, once complete, its metrics.curl -X POST https://api.dpf-it.com/data-specs \ -H "Authorization: Bearer YOUR_DPF_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "action": "get-status", "jobId": "550e8400-e29b-41d4-a716-446655440005" }' # 200 OK # { # "success": true, # "data": { # "status": "completed", # "metrics": { "recordsRead": 3, "recordsWritten": 3, "filesProcessed": 1 }, # ... # } # }
customers in this example)
with 4 audit columns appended automatically: dpf_line, dpf_filename,
dpf_job, dpf_ts. Confirm the table name with SHOW TABLES
in Objective 5 if you're not sure what it inferred.
3Load & Transform into an Existing Table
Still Flow A, but with targetOption: "existing-tables" instead of auto-infer: the
target schema is the customers table created in Objective 2, and the source file has
a different shape entirely — differently-named columns, a different date format, mixed casing.
additionalPrompt carries the field-mapping and transformation instructions, and
merge: true makes the load an upsert (by inferred primary key) instead of a
plain append, so re-running it with overlapping IDs doesn't create duplicates.
-
Create the spec against the existing table
curl -X POST https://api.dpf-it.com/data-specs \ -H "Authorization: Bearer YOUR_DPF_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "action": "create-spec", "workspaceId": "YOUR_WORKSPACE_ID", "specName": "Legacy CRM Backfill", "sampleFileName": "legacy_crm_export.csv", "targetOption": "existing-tables", "targetTables": ["customers"], "merge": true, "computeSize": "small", "additionalPrompt": "Source columns are CUST_ID, COMPANY_NAME, EMAIL_ADDR, SIGNUP_MMDDYYYY. Map CUST_ID -> id, COMPANY_NAME -> name, EMAIL_ADDR -> email (lowercased), and parse SIGNUP_MMDDYYYY (MM/DD/YYYY) into signup_date as an ISO 8601 date. Upsert on id.", "description": "Backfill customers from the legacy CRM export" }' # 201 Created — same signedUrls / specId shape as Objective 2 -
Upload the sample file
legacy_crm_export.csv— note the different column names, casing, and date format vs. the targetcustomerstable:CUST_ID,COMPANY_NAME,EMAIL_ADDR,SIGNUP_MMDDYYYY 1,ACME CORP,OPS@ACME.TEST,07/01/2026 4,Initech,billing@initech.test,07/03/2026curl -X PUT "SIGNED_URL_FROM_STEP_1" \ --upload-file legacy_crm_export.csv -
Start analysis
curl -X POST https://api.dpf-it.com/data-specs \ -H "Authorization: Bearer YOUR_DPF_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "action": "start-analysis", "workspaceId": "YOUR_WORKSPACE_ID", "specName": "Legacy CRM Backfill" }' -
Poll until ready, then poll the load
Same two-step polling as Objective 2 (
get-statusbyspecId, then by the resultinglastJobId). Becauseid: 1already exists incustomersfrom Objective 2, that row is updated in place (company casing normalized, email lowercased) —id: 4is inserted as new.
existing-tables only determines column mapping against a schema that
already exists — it does not create tables. If customers didn't already exist,
this call would fail; that's exactly what Objective 2's auto-infer flow is for.
4Connect an SFTP Server & Trigger a Daily Load
Automate Objective 2's spec so new files dropped on an SFTP server load themselves every day,
with no manual create-job/start-job calls. A connection
holds how DPF authenticates to the server; a trigger pairs that connection with
a spec and a schedule.
-
Create the connection
DPF generates an RSA-4096 keypair and returns the public half — it never returns the private
key.
curl -X POST https://api.dpf-it.com/connections \ -H "Authorization: Bearer YOUR_DPF_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "action": "create-connection", "workspaceId": "YOUR_WORKSPACE_ID", "type": "sftp", "hostname": "sftp.example.com", "username": "sftpuser" }' # 201 Created # { "success": true, "data": { "connectionId": "...", "publicKey": "ssh-rsa AAAA...", ... } } -
Install the public key on the SFTP server
As
sftpuseron the server (see the Integration Guide for the fullauthorized_keyspermissions walkthrough):mkdir -p ~/.ssh && chmod 700 ~/.ssh echo 'ssh-rsa AAAA... ' >> ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys -
Test the connection
A connection must pass this before it can be used in a trigger.
curl -X POST https://api.dpf-it.com/connections \ -H "Authorization: Bearer YOUR_DPF_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "action": "test-connection", "workspaceId": "YOUR_WORKSPACE_ID", "connectionId": "YOUR_CONNECTION_ID" }' # 200 OK — { "success": true, "data": { "success": true, ... } } -
Create the daily trigger
Points at the Objective 2 spec (
Customer Signups) — it must already have transformation code from a priorstart-analysis, which it does.preRules/postRulesare plain English; DPF compiles them into executable JS and returns it read-only aspreCode/postCode.curl -X POST https://api.dpf-it.com/job-triggers \ -H "Authorization: Bearer YOUR_DPF_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "action": "create-trigger", "workspaceId": "YOUR_WORKSPACE_ID", "type": "sftp", "specName": "Customer Signups", "connectionId": "YOUR_CONNECTION_ID", "frequency": { "unit": "daily", "hourOfDay": 6 }, "dedupe": true, "preRules": "Process only *.csv files under /outbound", "postRules": "Rename each processed file with a .done suffix" }' # 201 Created — { "success": true, "data": { "triggerId": "...", "preCode": "...", "postCode": "...", ... } } -
(Optional) Fire it once immediately to test
Don't want to wait for 06:00 UTC to confirm it works end-to-end:
curl -X POST https://api.dpf-it.com/job-triggers \ -H "Authorization: Bearer YOUR_DPF_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "action": "run-trigger-now", "workspaceId": "YOUR_WORKSPACE_ID", "triggerId": "YOUR_TRIGGER_ID" }' # 202 Accepted — the run shows up under list-trigger-runs on /workspaces
frequency — no more
manual calls needed for day-to-day loads. Each run stamps its rows with dpf_ts,
which is exactly what Objective 6 filters on to find "what's new."
5Query the Fully Loaded Data
POST /query runs SQL directly against your Iceberg tables, scoped to a single
namespace — the trailing 12 characters of workspaceId (see the
callout in Objective 1). This objective covers reads — SELECT,
DESCRIBE, and SHOW TABLES; Objective 6 covers the write side.
-
Confirm the table name
curl -X POST https://api.dpf-it.com/query \ -H "Authorization: Bearer YOUR_DPF_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "namespace": "446655440099", "query": "SHOW TABLES" }' -
Select rows
curl -X POST https://api.dpf-it.com/query \ -H "Authorization: Bearer YOUR_DPF_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "namespace": "446655440099", "query": "SELECT * FROM customers ORDER BY dpf_ts DESC LIMIT 10" }' # 200 OK # { # "success": true, # "data": { # "columns": ["id", "name", "email", "signup_date", "dpf_line", "dpf_filename", "dpf_job", "dpf_ts"], # "rows": [ { "id": "4", "name": "Initech", "email": "billing@initech.test", ... } ], # "rowCount": 1 # } # } -
Count and aggregate
dpf_filename/dpf_jobmake it easy to see how much each load contributed:curl -X POST https://api.dpf-it.com/query \ -H "Authorization: Bearer YOUR_DPF_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "namespace": "446655440099", "query": "SELECT dpf_filename, COUNT(*) AS rows_loaded FROM customers GROUP BY dpf_filename ORDER BY rows_loaded DESC" }'
POST /query-large is a drop-in replacement with 4× the memory/CPU and a 120s
timeout (vs. 60s), for large scans or heavy joins/aggregations. It costs 4× the credits per call.
6Query Incrementally Loaded Data & Merge into an Aggregate Table
This objective uses a capability the rest of this page hasn't needed: POST /query
accepts real write SQL too — INSERT, UPDATE, DELETE, and
MERGE — for any workspace member with full access (not
read-only), committed durably to the Iceberg table. That makes the whole incremental-aggregate
pattern a single statement: read the rows added to customers since the last run,
and MERGE them straight into the aggregate table.
"Since the last run" is a plain dpf_ts BETWEEN window on the audit column every
load already stamps.
-
One-time: create the aggregate table
No need to route this through
/data-specs— since you already know the target shape, create it directly against the Iceberg REST catalog. It's synchronous, no sample file or AI step involved, and comes back with zero snapshots (empty).
Requires full permission on the workspace, same ascurl -X POST https://api.dpf-it.com/iceberg/v1/namespaces/446655440099/tables \ -H "Authorization: Bearer YOUR_DPF_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "customer_signups_daily", "schema": { "type": "struct", "fields": [ { "id": 1, "name": "signup_date", "type": "date", "required": true }, { "id": 2, "name": "total_signups", "type": "long", "required": true } ] } }' # 200 OK — { "metadata-location": "...", "metadata": { "current-snapshot-id": -1, "snapshots": [], ... } }create-spec. -
Set the run window as shell variables
From here on, this objective is a short chain of commands, so pull the moving parts into
shell variables instead of repeating literals.
RUN_TSis captured fresh each run, before the merge executes;LAST_RUN_TSis whateverRUN_TSwas set to at the end of the previous run (see the last step below) — first run only, seed it with a floor from before Objective 2's initial load.export DPF_TOKEN="YOUR_DPF_JWT_TOKEN" export NAMESPACE="446655440099" RUN_TS=$(date -u +"%Y-%m-%d %H:%M:%S") LAST_RUN_TS="2026-07-01 00:00:00" # first run only; later runs reuse the previous RUN_TS -
Merge the change window into the aggregate table
One statement: a plain
dpf_ts BETWEENfilter isolates the rows loaded in this window, grouped bysignup_dateand joined against the target —WHEN MATCHEDadds to an existing day's total,WHEN NOT MATCHEDinserts a new day. Sent to/queryas thequeryfield, with$NAMESPACE,$LAST_RUN_TS, and$RUN_TSinterpolated in via an unquoted heredoc;\ninside thequerystring keeps the statement readable without repeating it outside the JSON:curl -X POST https://api.dpf-it.com/query \ -H "Authorization: Bearer $DPF_TOKEN" \ -H "Content-Type: application/json" \ -d @- <<EOF { "namespace": "$NAMESPACE", "query": "MERGE INTO customer_signups_daily AS tgt\nUSING (\n SELECT signup_date, COUNT(*) AS new_signups\n FROM customers\n WHERE dpf_ts BETWEEN TIMESTAMP '$LAST_RUN_TS' AND TIMESTAMP '$RUN_TS'\n GROUP BY signup_date\n) AS src\nON tgt.signup_date = src.signup_date\nWHEN MATCHED THEN UPDATE SET total_signups = tgt.total_signups + src.new_signups\nWHEN NOT MATCHED THEN INSERT (signup_date, total_signups)\n VALUES (src.signup_date, src.new_signups)" } EOF # 200 OK — { "success": true, "data": { "columns": [], "rows": [], "rowCount": 0 } }, write committed LAST_RUN_TS=$RUN_TS # ready for the next runBETWEENis inclusive on both ends — a row landing exactly onRUN_TSwould be picked up again by the next run'sLAST_RUN_TS. In practicedpf_tscarries sub-second precision and each run captures its own freshRUN_TS, so an exact collision is vanishingly unlikely — use>/<=instead ofBETWEENif you need that edge airtight. DPF doesn't track per-caller checkpoints for you — persistingLAST_RUN_TSacross runs (a file, a small DynamoDB row, whatever) is on whatever schedules the next run.
INSERT/UPDATE/DELETE/MERGE) via
/query is rejected with 403 for workspace members whose permission is
read-only — only owner/full members can write. The same
per-request, namespace-scoped credentials that confine reads to your workspace also confine
writes, so there's no separate write-access boundary to reason about.
MERGE the delta, then persist RUN_TS as the next run's
LAST_RUN_TS. Step 1 only ever runs once.