GET
/
api
/
v1
/
me
/
export
/
:jobId
Get GDPR export status
curl --request GET \
  --url https://api.example.com/api/v1/me/export/:jobId
import requests

url = "https://api.example.com/api/v1/me/export/:jobId"

response = requests.get(url)

print(response.text)
const options = {method: 'GET'};

fetch('https://api.example.com/api/v1/me/export/:jobId', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));
<?php

$curl = curl_init();

curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/api/v1/me/export/:jobId",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
]);

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
package main

import (
"fmt"
"net/http"
"io"
)

func main() {

url := "https://api.example.com/api/v1/me/export/:jobId"

req, _ := http.NewRequest("GET", url, nil)

res, _ := http.DefaultClient.Do(req)

defer res.Body.Close()
body, _ := io.ReadAll(res.Body)

fmt.Println(string(body))

}
HttpResponse<String> response = Unirest.get("https://api.example.com/api/v1/me/export/:jobId")
.asString();
require 'uri'
require 'net/http'

url = URI("https://api.example.com/api/v1/me/export/:jobId")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Get.new(url)

response = http.request(request)
puts response.read_body

Overview

Polls a single gdpr_export_jobs row by id. Used by clients that want to show a “your data is being prepared” UI between export request and the email arriving. When the job reaches READY and the row has not yet expired (expiresAt > now), the endpoint mints a fresh 24h signed download URL on every call — so a user who lost the original email link can still download via this endpoint as long as the underlying row is alive.
The signed URL TTL is 24 hours. The email link uses the same TTL — but since the URL is re-minted on every poll, the practical download window for the user is as long as the gdpr_export_jobs row exists (up to 30 days, then it’s cleaned up by gdpr-erasure along with the underlying S3 object).

Authentication

Bearer <accessToken> (user scope) required. The route also gates on requireOnboarded. The job is scoped to the authenticated user — calling with another user’s jobId returns 404 NOT_FOUND (enumeration-safe — we never reveal that the job belongs to someone else).

Path parameters

FieldTypeRequiredNotesExample
jobIdstringyesReturned by GET /me/export.665b1c8d9c2b0a0001a4f701

Query parameters

None.

Request body

None — this is a GET.

Response — 200 OK

FieldTypeNotes
jobIdstringEcho of path param.
statusstringOne of PENDING, PROCESSING, READY, FAILED, EXPIRED.
requestedAtstring (ISO 8601)When the user pressed the button.
completedAtstring | nullWhen the worker finished. Present for READY, FAILED, and EXPIRED.
downloadUrlstring | nullA freshly-minted 24h signed S3 URL. Only present when status === 'READY' and the row has not expired. Null otherwise.
expiresAtstring | nullWhen the underlying gdpr_export_jobs row will be considered EXPIRED (and its S3 object eligible for cleanup). Present for READY.

Example — PENDING

{
  "jobId": "665b1c8d9c2b0a0001a4f701",
  "status": "PENDING",
  "requestedAt": "2026-05-23T08:00:00.000Z",
  "completedAt": null,
  "downloadUrl": null,
  "expiresAt": null
}

Example — READY

{
  "jobId": "665b1c8d9c2b0a0001a4f701",
  "status": "READY",
  "requestedAt": "2026-05-23T08:00:00.000Z",
  "completedAt": "2026-05-23T08:00:42.123Z",
  "downloadUrl": "https://swappr-private.lon1.cdn.digitaloceanspaces.com/gdpr-exports/664a01.../665b1c8d.zip?X-Amz-Signature=...",
  "expiresAt":   "2026-06-22T08:00:42.123Z"
}

Example — FAILED

{
  "jobId": "665b1c8d9c2b0a0001a4f701",
  "status": "FAILED",
  "requestedAt": "2026-05-23T08:00:00.000Z",
  "completedAt": "2026-05-23T08:00:05.987Z",
  "downloadUrl": null,
  "expiresAt": null
}

Error responses

StatusCodeMeaning
400VALIDATION_FAILEDjobId missing or malformed path segment.
401UNAUTHENTICATEDMissing / malformed / expired token.
403ONBOARDING_INCOMPLETEThe caller has not finished onboarding.
404NOT_FOUNDNo job exists with that id for this user. Other users’ jobs are returned as 404 — enumeration-safe.

Example error — 404 NOT_FOUND

{
  "type": "https://api.swappr.co.uk/errors/not-found",
  "title": "Resource not found",
  "status": 404,
  "code": "NOT_FOUND",
  "detail": "GDPR export job not found",
  "instance": "/api/v1/me/export/665b1c8d9c2b0a0001a4f701",
  "requestId": "01HZQ7K3M4N5P6Q7R8S9T0V1W2"
}

Polling guidance

The export worker typically completes in seconds for a fresh account, up to a minute for a heavily-active user. Suggested client polling:
  • Initial wait: 2 seconds.
  • Then poll every 5 seconds until status === 'READY' or status === 'FAILED'.
  • Stop polling after 5 minutes — show the user “we’ll email you when it’s ready” and rely on the email.
The worker emails the user on READY with a one-shot 24h link, so a user who closes the page still receives their data.

See also

curl

curl -X GET https://api.swappr.co.uk/api/v1/me/export/665b1c8d9c2b0a0001a4f701 \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Postman

See docs/postman/swappr.postman_collection.jsonMe → Export Status.